官方 Demo:https://vueuse.org/core/useScroll/#usescroll
昨天看了 Demo 中的 X position
、Y position
是怎麼來的,接下來繼續看 Demo 中 Top Arrived
、Right Arrived
、Bottom Arrived
、Left Arrived
這四個 Boolean 的計算方式。
Top Arrived
、Right Arrived
、Bottom Arrived
、Left Arrived
這四個 Boolean 是來自於 arrivedState
這個 reactive 物件。
來看一下相關程式碼:
const ARRIVED_STATE_THRESHOLD_PIXELS = 1
export function useScroll(element, options = {}) {
const {
offset = {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
} = options
const arrivedState = reactive({
left: true,
right: false,
top: true,
bottom: false,
})
// ...略
const setArrivedState = (target) => {
if (!window)
return
const el = (
(target)?.document?.documentElement
|| (target)?.documentElement
|| unrefElement(target)
)
const { display, flexDirection } = getComputedStyle(el)
const scrollLeft = el.scrollLeft
const left = Math.abs(scrollLeft) <= (offset.left || 0)
const right = Math.abs(scrollLeft)
+ el.clientWidth >= el.scrollWidth
- (offset.right || 0)
- ARRIVED_STATE_THRESHOLD_PIXELS
if (display === 'flex' && flexDirection === 'row-reverse') {
arrivedState.left = right
arrivedState.right = left
}
else {
arrivedState.left = left
arrivedState.right = right
}
let scrollTop = el.scrollTop
// patch for mobile compatible
if (target === window.document && !scrollTop)
scrollTop = window.document.body.scrollTop
const top = Math.abs(scrollTop) <= (offset.top || 0)
const bottom = Math.abs(scrollTop)
+ el.clientHeight >= el.scrollHeight
- (offset.bottom || 0)
- ARRIVED_STATE_THRESHOLD_PIXELS
if (display === 'flex' && flexDirection === 'column-reverse') {
arrivedState.top = bottom
arrivedState.bottom = top
}
else {
arrivedState.top = top
arrivedState.bottom = bottom
}
}
const onScrollHandler = (e) => {
if (!window)
return
const eventTarget = (
(e.target).documentElement ?? e.target
)
setArrivedState(eventTarget)
}
useEventListener(
element,
'scroll',
onScrollHandler,
eventListenerOptions,
)
return {
x,
y,
arrivedState,
}
}
水平、垂直捲軸的計算邏輯是一樣的,先聚焦在水平捲軸的計算:
const left = Math.abs(scrollLeft) <= (offset.left || 0)
const right = Math.abs(scrollLeft)
+ el.clientWidth >= el.scrollWidth
- (offset.right || 0)
- ARRIVED_STATE_THRESHOLD_PIXELS
left
的計算滿單純的,scrollLeft 是 0 的時候,捲軸一定在最左邊。
那 Math.abs 是怎麼回事?這邊用絕對值是因為 iOS safari 中預設的捲軸行為,可以嘗試用 iphone safari 開啟官方 Demo,一直把捲軸往左捲動,會發現 Position X
變為負值,這時候 Left Arrived
也會是 false。在 Android 中則沒有負數的問題。
offset.left 又是什麼?可以看到上面參數多了 offset
這個 option,用途是,假設我們想讓 Left Arrived
在捲軸離最左邊 5px 的時候就變為 true,而不是真的完全貼到最左邊(0px)的時候才變成 true,就可以設定 offset.left = 5 來達成這個效果。有點像是留一個安全距離的感覺,對我們彼此都比較好(?)
right
的計算就比較複雜一點,先來看一張醜醜但有用的圖:
可見區域就是我們的 target,可以先把它想成視窗大小,左右的空白區域就是超出視窗的部分,現在假設我們把捲軸往右滾到最底,右邊那塊空白區域就會進到可見區域中(右邊空白區域沒了),但可見區域大小是固定的,所以原本在可見區域中的部分畫面也會被移到可見區域左邊的那塊空白大小(左邊空白區域變大),在捲軸滾到最右邊的情況下,clientWidth + scrollLeft 會大約等於 scrollWidth。
為什麼要減掉 ARRIVED_STATE_THRESHOLD_PIXELS
(1px)?
根據 mdn 解釋,因為 scrollHeight
、clientHeight
都是四捨五入的數字,而 scrollTop
有可能會是浮點數,以官方範例來說:
element.scrollHeight - Math.abs(element.scrollTop) === element.clientHeight;
以上判斷不一定總是能判斷出已經捲動到最底部,所以判斷應該要給一個空間(阿縮比):
Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <= 1;
到這邊我們 left
、right
的計算都看完了,不過還有有方向的問題要注意:
const { display, flexDirection } = getComputedStyle(el)
if (display === 'flex' && flexDirection === 'row-reverse') {
arrivedState.left = right
arrivedState.right = left
}
else {
arrivedState.left = left
arrivedState.right = right
}
這段滿直覺的,就是有用 flex 排版並調換方向的時候,左右狀態需要互換。top
、bottom
計算概念是一樣的,就不另外寫上來了~
GitHub PR:https://github.com/RhinoLee/30days_vue/pull/20/files
今天看完 useScroll arrivedState 的部分,結果 useScroll 真的要有 part3 了 XD,明天會把剩下的 scrolling 相關功能看完(吧)